1 using System;
2 using System.Collections.Generic;
3 using UnityEngine;
4 using System.Collections;
5 using Random = UnityEngine.Random;
6
7 namespace ProceduralToolkit.Examples
8 {
9 /// <summary>
10 /// A single-mesh particle system with birds-like behaviour
11 /// </summary>
12 /// <remarks>
13 /// http://en.wikipedia.org/wiki/Boids
14 /// </remarks>
15 public class BoidController
16 {
17 [Serializable]
18 public class Config
19 {
20 public Vector3 anchor = Vector3.zero;
21 public float spawnSphere = 10;
22 public float worldSphere = 15;
23
24 public int swarmCount = 2000;
25 public int maxSpeed = 10;
26 public float interactionRadius = 5;
27 public float cohesionCoefficient = 1;
28 public float separationDistance = 3;
29 public float separationCoefficient = 10;
30 public float alignmentCoefficient = 5;
31
32 /// <summary>
33 /// Number of neighbours participating in calculations
34 /// </summary>
35 public int maxBoids = 5;
36 /// <summary>
37 /// Percentage of swarm simulated in each frame
38 /// </summary>
39 public float simulationPercent = 0.01f;
40
41 public MeshDraft template;
42 }
43
44 private Config config;
45 private List<Boid> boids = new List<Boid>();
46 private MeshDraft draft;
47 private Mesh mesh;
48 private List<Boid> neighbours = new List<Boid>();
49 private int maxSimulationSteps;
50
51 /// <summary>
52 /// Generate new colors and positions for boids
53 /// </summary>
54 public Mesh Generate(Config config)
55 {
56 this.config = config;
57
58 // Avoid vertex count overflow
59 config.swarmCount = Mathf.Min(65000/config.template.vertexCount, config.swarmCount);
60 // Optimization trick: in each frame we simulate only small percent of all boids
61 maxSimulationSteps = Mathf.RoundToInt(config.swarmCount*config.simulationPercent);
62 int vertexCount = config.swarmCount*config.template.vertexCount;
63
64 draft = new MeshDraft
65 {
66 name = "Boids",
67 vertices = new List<Vector3>(vertexCount),
68 triangles = new List<int>(vertexCount),
69 normals = new List<Vector3>(vertexCount),
70 uv = new List<Vector2>(vertexCount),
71 colors = new List<Color>(vertexCount)
72 };
73
74 for (var i = 0; i < config.swarmCount; i++)
75 {
76 // Assign random starting values for each boid
77 var boid = new Boid
78 {
79 position = Random.insideUnitSphere*config.spawnSphere,
80 rotation = Random.rotation,
81 velocity = Random.onUnitSphere*config.maxSpeed
82 };
83 boids.Add(boid);
84
85 draft.Add(config.template);
86 }
87
88 mesh = draft.ToMesh();
89 mesh.MarkDynamic();
90 // Set bounds manually for correct culling
91 mesh.bounds = new Bounds(Vector3.zero, Vector3.one*config.worldSphere*2);
92 return mesh;
93 }
94
95 /// <summary>
96 /// Run simulation
97 /// </summary>
98 public IEnumerator CalculateVelocities()
99 {
100 int simulationStep = 0;
101
102 for (int currentIndex = 0; currentIndex < boids.Count; currentIndex++)
103 {
104 // Optimization trick: in each frame we simulate only small percent of all boids
105 simulationStep++;
106 if (simulationStep > maxSimulationSteps)
107 {
108 simulationStep = 0;
109 yield return null;
110 }
111
112 var boid = boids[currentIndex];
113 // Search for nearest neighbours
114 neighbours.Clear();
115 for (int i = 0; i < boids.Count; i++)
116 {
117 Boid neighbour = boids[i];
118
119 Vector3 toNeighbour = neighbour.position - boid.position;
120 if (toNeighbour.sqrMagnitude < config.interactionRadius)
121 {
122 neighbours.Add(neighbour);
123 if (neighbours.Count == config.maxBoids)
124 {
125 break;
126 }
127 }
128 }
129
130 if (neighbours.Count < 2) continue;
131
132 boid.velocity = Vector3.zero;
133 boid.cohesion = Vector3.zero;
134 boid.separation = Vector3.zero;
135 boid.alignment = Vector3.zero;
136
137 // Calculate boid parameters
138 int separationCount = 0;
139 for (int i = 0; i < neighbours.Count && i < config.maxBoids; i++)
140 {
141 Boid neighbour = neighbours[i];
142
143 boid.cohesion += neighbour.position;
144 boid.alignment += neighbour.velocity;
145
146 Vector3 toNeighbour = neighbour.position - boid.position;
147 if (toNeighbour.sqrMagnitude > 0 &&
148 toNeighbour.sqrMagnitude < config.separationDistance*config.separationDistance)
149 {
150 boid.separation += toNeighbour/toNeighbour.sqrMagnitude;
151 separationCount++;
152 }
153 }
154
155 // Clamp all parameters to safe values
156 boid.cohesion /= Mathf.Min(neighbours.Count, config.maxBoids);
157 boid.cohesion = Vector3.ClampMagnitude(boid.cohesion - boid.position, config.maxSpeed);
158 boid.cohesion *= config.cohesionCoefficient;
159
160 if (separationCount > 0)
161 {
162 boid.separation /= separationCount;
163 boid.separation = Vector3.ClampMagnitude(boid.separation, config.maxSpeed);
164 boid.separation *= config.separationCoefficient;
165 }
166
167 boid.alignment /= Mathf.Min(neighbours.Count, config.maxBoids);
168 boid.alignment = Vector3.ClampMagnitude(boid.alignment, config.maxSpeed);
169 boid.alignment *= config.alignmentCoefficient;
170
171 // Calculate resulting velocity
172 Vector3 velocity = boid.cohesion + boid.separation + boid.alignment;
173 boid.velocity = Vector3.ClampMagnitude(velocity, config.maxSpeed);
174 if (boid.velocity == Vector3.zero)
175 {
176 // Prevent boids from stopping
177 boid.velocity = Random.onUnitSphere*config.maxSpeed;
178 }
179 }
180 }
181
182 /// <summary>
183 /// Apply simulation to mesh
184 /// </summary>
185 public void Update()
186 {
187 for (int i = 0; i < boids.Count; i++)
188 {
189 var boid = boids[i];
190 boid.rotation = Quaternion.FromToRotation(Vector3.up, boid.velocity);
191
192 // Contain boids in sphere
193 Vector3 distanceToAnchor = config.anchor - boid.position;
194 if (distanceToAnchor.sqrMagnitude > config.worldSphere*config.worldSphere)
195 {
196 boid.velocity += distanceToAnchor/config.worldSphere;
197 boid.velocity = Vector3.ClampMagnitude(boid.velocity, config.maxSpeed);
198 }
199
200 boid.position += boid.velocity*Time.deltaTime;
201 SetBoidVertices(boid, i);
202 }
203 mesh.SetVertices(draft.vertices);
204 mesh.RecalculateNormals();
205 }
206
207 private void SetBoidVertices(Boid boid, int boidIndex)
208 {
209 for (int i = 0; i < config.template.vertexCount; i++)
210 {
211 int vertexIndex = boidIndex*config.template.vertexCount + i;
212 draft.vertices[vertexIndex] = boid.rotation*config.template.vertices[i] + boid.position;
213 }
214 }
215
216 private class Boid
217 {
218 public Vector3 position;
219 public Quaternion rotation;
220 public Vector3 velocity;
221 public Vector3 cohesion;
222 public Vector3 separation;
223 public Vector3 alignment;
224 }
225 }
226 }